﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Ionic.Zip;
using System.Reflection;
using System.Globalization;
using System.IO;
using System.Data;

namespace cz.muni.fi.pb138.IO
{
    class IODataSourceODF : IIODataSource
    {

        public string FileName { get; set; }
        private string sheetName = "Movie List";

        #region namespace handling


        /// <summary>
		/// Namespaces. We need this to initialize XmlNamespaceManager so that we can search XmlDocument.
        /// </summary>
        private static string[,] namespaces = new string[,] 
                {
                    {"table", "urn:oasis:names:tc:opendocument:xmlns:table:1.0"},
                    {"office", "urn:oasis:names:tc:opendocument:xmlns:office:1.0"},
                    {"style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0"},
                    {"text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0"},            
                    {"draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"},
                    {"fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"},
                    {"dc", "http://purl.org/dc/elements/1.1/"},
                    {"meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0"},
                    {"number", "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"},
                    {"presentation", "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0"},
                    {"svg", "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"},
                    {"chart", "urn:oasis:names:tc:opendocument:xmlns:chart:1.0"},
                    {"dr3d", "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0"},
                    {"math", "http://www.w3.org/1998/Math/MathML"},
                    {"form", "urn:oasis:names:tc:opendocument:xmlns:form:1.0"},
                    {"script", "urn:oasis:names:tc:opendocument:xmlns:script:1.0"},
                    {"ooo", "http://openoffice.org/2004/office"},
                    {"ooow", "http://openoffice.org/2004/writer"},
                    {"oooc", "http://openoffice.org/2004/calc"},
                    {"dom", "http://www.w3.org/2001/xml-events"},
                    {"xforms", "http://www.w3.org/2002/xforms"},
                    {"xsd", "http://www.w3.org/2001/XMLSchema"},
                    {"xsi", "http://www.w3.org/2001/XMLSchema-instance"},
                    {"rpt", "http://openoffice.org/2005/report"},
                    {"of", "urn:oasis:names:tc:opendocument:xmlns:of:1.2"},
                    {"rdfa", "http://docs.oasis-open.org/opendocument/meta/rdfa#"},
                    {"config", "urn:oasis:names:tc:opendocument:xmlns:config:1.0"}
                };

		/// <summary>
		/// Initialises the XML namespace manager
		/// </summary>
		/// <param name="xmlDocument">the document of which the namespace should be initialised</param>
		/// <returns>namespace manager of the given document</returns>
        private XmlNamespaceManager InitializeXmlNamespaceManager(XmlDocument xmlDocument)
        {
            XmlNamespaceManager nmsManager = new XmlNamespaceManager(xmlDocument.NameTable);

            for (int i = 0; i < namespaces.GetLength(0); i++)
                nmsManager.AddNamespace(namespaces[i, 0], namespaces[i, 1]);

            return nmsManager;
        }

		/// <summary>
		/// retrieves the URI of a given prefix
		/// </summary>
		/// <param name="prefix">a prefix a namespace of which is being serched for</param>
		/// <returns>the found prefix</returns>
		/// <exception cref="InvalidOperationException">trown if no appropriate URI is found</exception>
        private string GetNamespaceUri(string prefix)
        {
            for (int i = 0; i < namespaces.GetLength(0); i++)
            {
                if (namespaces[i, 0] == prefix)
                    return namespaces[i, 1];
            }

            throw new InvalidOperationException("Can't find that namespace URI");
        }


        #endregion




        #region saving data


        /// <summary>
		/// containing Column data: [Name], [cell style], [Default cell style]
        /// </summary>
        private static string[,] columnTypes = new string[,] 
                {
                    {"Original Name", "co1", "Default"},
                    {"Translated Name","co1", "Default"},
                    {"Country","co2", "Default"},
                    {"Release Date","co3", "ce1"},
                    {"Length (min)","co4", "ce2"},
                    {"Director","co1", "Default"},
                    {"Rating","co5", "ce2"},
                    {"Genres","co2", "Default"},
                    {"Cast","co1", "Default"},
                    {"Summary of content","co6", "Default"}
                };

		/// <summary>
		/// Stores a given media list into the file
		/// </summary>
		/// <param name="media">a collection of <c>Media</c> classes to be stored</param>
        public void storeMedia(IList<Media> media)
        {

            ZipFile templateFile = this.GetZipFile(Assembly.GetExecutingAssembly().GetManifestResourceStream("cz.muni.fi.pb138.template.ods"));

            XmlDocument contentXml = this.GetContentXmlFile(templateFile);
            XmlNamespaceManager nmsManager = this.InitializeXmlNamespaceManager(contentXml);
            XmlNode sheetsRootNode = this.GetSheetsRootNodeAndRemoveChildrens(contentXml, nmsManager);


            // Save main sheet
            {
                XmlDocument ownerDocument = sheetsRootNode.OwnerDocument;

                XmlNode sheetNode = ownerDocument.CreateElement("table:table", this.GetNamespaceUri("table"));

                XmlAttribute sheetName = ownerDocument.CreateAttribute("table:name", this.GetNamespaceUri("table"));
                sheetName.Value = this.sheetName;
                sheetNode.Attributes.Append(sheetName);

                // Save columns definition
                this.SaveColumnsDefinition(sheetNode, ownerDocument);


                // Save medias    
                for (int i = 0; i < media.Count; i++)
                {
                    XmlNode rowNode = ownerDocument.CreateElement("table:table-row", this.GetNamespaceUri("table"));

                    XmlAttribute rowStyle = ownerDocument.CreateAttribute("table:style-name", this.GetNamespaceUri("table"));
                    rowStyle.Value = "ro1";
                    rowNode.Attributes.Append(rowStyle);


                    this.CreateCell(media[i].OriginalName, rowNode, ownerDocument);
                    this.CreateCell(media[i].TranslatedName, rowNode, ownerDocument);
                    this.CreateCell(media[i].Country, rowNode, ownerDocument);
                    this.CreateCell(media[i].ReleaseDate, rowNode, ownerDocument);
                    this.CreateCell((float)media[i].Length, rowNode, ownerDocument);
                    this.CreateCell(media[i].Director, rowNode, ownerDocument);
                    this.CreateCell((float)media[i].Rating, rowNode, ownerDocument);

                    this.CreateCell(media[i].Genres, rowNode, ownerDocument);
                    this.CreateCell(media[i].Cast, rowNode, ownerDocument);

                    this.CreateCell(media[i].Content, rowNode, ownerDocument);

                    sheetNode.AppendChild(rowNode);
                }

                sheetsRootNode.AppendChild(sheetNode);
            }

            this.SaveContentXml(templateFile, contentXml);
            templateFile.Save(this.FileName);
        }

		/// <summary>
		/// saves the columns definitions, ie. creates the names of each column in the file
		/// </summary>
		/// <param name="sheetNode">the name of a sheet into which the definition should be placed</param>
		/// <param name="ownerDocument">the document into which the definition is placed</param>
        private void SaveColumnsDefinition(XmlNode sheetNode, XmlDocument ownerDocument)
        {

            for (int i = 0; i < columnTypes.GetLength(0); i++)
            {
                XmlNode columnDefinition = ownerDocument.CreateElement("table:table-column", this.GetNamespaceUri("table"));

                XmlAttribute columnsDefStyle = ownerDocument.CreateAttribute("default-cell-style-name", this.GetNamespaceUri("table"));
                columnsDefStyle.Value = columnTypes[i, 2];
                columnDefinition.Attributes.Append(columnsDefStyle);

                XmlAttribute columnsStyle = ownerDocument.CreateAttribute("table:style-name", this.GetNamespaceUri("table"));
                columnsStyle.Value = columnTypes[i, 1];
                columnDefinition.Attributes.Append(columnsStyle);

                sheetNode.AppendChild(columnDefinition);
            }


            // Create first line with labels
            {
                XmlNode rowNode = ownerDocument.CreateElement("table:table-row", this.GetNamespaceUri("table"));

                XmlAttribute rowStyle = ownerDocument.CreateAttribute("table:style-name", this.GetNamespaceUri("table"));
                rowStyle.Value = "ro1";
                rowNode.Attributes.Append(rowStyle);

                for (int i = 0; i < columnTypes.GetLength(0); i++)
                    this.CreateCell(columnTypes[i, 0], rowNode, ownerDocument);

                sheetNode.AppendChild(rowNode);
            }

        }

		/// <summary>
		/// Creates a single cell in the document and fills it with the given <value>string</value> value
		/// </summary>
		/// <param name="Val">the value to be paced in the cell</param>
		/// <param name="rowNode">the node into which the value will be stored</param>
		/// <param name="ownerDocument">the document in which the cell is created</param>
        private void CreateCell(string Val, XmlNode rowNode, XmlDocument ownerDocument)
        {

            XmlElement cellNode = ownerDocument.CreateElement("table:table-cell", this.GetNamespaceUri("table"));

            // Save values as text or string
            XmlAttribute valueType = ownerDocument.CreateAttribute("office:value-type", this.GetNamespaceUri("office"));
            valueType.Value = "string";
            cellNode.Attributes.Append(valueType);

            XmlElement cellValue = ownerDocument.CreateElement("text:p", this.GetNamespaceUri("text"));
            cellValue.InnerText = Val;
            cellNode.AppendChild(cellValue);

            rowNode.AppendChild(cellNode);

        }

		/// <summary>
		/// Creates a single cell in the document and fills it with the given <value>float</value> value
		/// </summary>
		/// <param name="Val">the float value to be stored</param>
		/// <param name="rowNode">the node into which the value will be stored</param>
		/// <param name="ownerDocument">the document in which the cell is created</param>
        private void CreateCell(float Val, XmlNode rowNode, XmlDocument ownerDocument)
        {

            XmlElement cellNode = ownerDocument.CreateElement("table:table-cell", this.GetNamespaceUri("table"));

            XmlAttribute valueType = ownerDocument.CreateAttribute("office:value-type", this.GetNamespaceUri("office"));
            valueType.Value = "float";
            cellNode.Attributes.Append(valueType);

            XmlAttribute officeValue = ownerDocument.CreateAttribute("office:value", this.GetNamespaceUri("office"));
            officeValue.Value = Val.ToString("G", CultureInfo.CreateSpecificCulture("en-US"));
            cellNode.Attributes.Append(officeValue);

            XmlElement cellValue = ownerDocument.CreateElement("text:p", this.GetNamespaceUri("text"));
            cellValue.InnerText = Val.ToString();
            cellNode.AppendChild(cellValue);

            rowNode.AppendChild(cellNode);

        }

		/// <summary>
		/// Creates a single cell in the document and fills it with the given <value>DateTime</value> value
		/// </summary>
		/// <param name="Val">the <value>DateTime</value> value to be stored</param>
		/// <param name="rowNode">the node into which the value will be stored</param>
		/// <param name="ownerDocument">the document in which the cell is created</param>
        private void CreateCell(DateTime Val, XmlNode rowNode, XmlDocument ownerDocument)
        {

            XmlElement cellNode = ownerDocument.CreateElement("table:table-cell", this.GetNamespaceUri("table"));

            XmlAttribute valueType = ownerDocument.CreateAttribute("office:value-type", this.GetNamespaceUri("office"));
            valueType.Value = "date";
            cellNode.Attributes.Append(valueType);

            XmlAttribute officeValue = ownerDocument.CreateAttribute("office:date-value", this.GetNamespaceUri("office"));
            officeValue.Value = Val.ToString("yyyy-MM-dd");
            cellNode.Attributes.Append(officeValue);

            XmlElement cellValue = ownerDocument.CreateElement("text:p", this.GetNamespaceUri("text"));
            cellValue.InnerText = Val.ToString("d. MMMM yyyy", CultureInfo.InvariantCulture);
            cellNode.AppendChild(cellValue);

            rowNode.AppendChild(cellNode);

        }


		/// <summary>
		/// Creates a single cell in the document and fills it with the given <value>IList</value> of <value>string</value> values
		/// </summary>
		/// <param name="Val">the <value>IList</value> of <value>strings</value> value to be stored</param>
		/// <param name="rowNode">the node into which the value will be stored</param>
		/// <param name="ownerDocument">the document in which the cell is created</param>
        private void CreateCell(IList<string> Vals, XmlNode rowNode, XmlDocument ownerDocument)
        {

            XmlElement cellNode = ownerDocument.CreateElement("table:table-cell", this.GetNamespaceUri("table"));

            XmlAttribute valueType = ownerDocument.CreateAttribute("office:value-type", this.GetNamespaceUri("office"));
            valueType.Value = "string";
            cellNode.Attributes.Append(valueType);

            XmlElement cellValue = ownerDocument.CreateElement("text:p", this.GetNamespaceUri("text"));
            string helpVal = "";
            foreach (string item in Vals)
            {
                helpVal += item;
                if (!object.ReferenceEquals(item, Vals.Last()))
                    helpVal += " | ";
            }
            cellValue.InnerText = helpVal;
            cellNode.AppendChild(cellValue);

            rowNode.AppendChild(cellNode);

        }

		/// <summary>
		/// Stores the whole document into the given Zip file. This is for the ODS file is a zip archive
		/// containing the definition of all the sheets and corresponding XML documents with contents definitions
		/// </summary>
		/// <param name="templateFile">the target zip archive</param>
		/// <param name="contentXml">the XML document to be saved</param>
        private void SaveContentXml(ZipFile templateFile, XmlDocument contentXml)
        {
            templateFile.RemoveEntry("content.xml");

            MemoryStream memStream = new MemoryStream();
            contentXml.Save(memStream);
            memStream.Seek(0, SeekOrigin.Begin);

            templateFile.AddEntry("content.xml", memStream);
        }

        #endregion




        #region reading data
		/// <summary>
		/// reads a <c>Media</c> from the <c>FileName</c>
		/// </summary>
		/// <returns>collection of all the media stored in the <c>FileName</c></returns>
        public IList<Media> readMedia()
        {

            bool correctFileFormat = false;
            ZipFile odsZipFile = this.GetZipFile(this.FileName);


            XmlDocument contentXml = this.GetContentXmlFile(odsZipFile);
            XmlNamespaceManager nmsManager = this.InitializeXmlNamespaceManager(contentXml);


            List<Media> media = new List<Media>();


            // iterate throughout all sheets in document and choose one with the media
            foreach (XmlNode tableNode in this.GetTableNodes(contentXml, nmsManager))
            {
                DataTable sheet = new DataTable();
                if (tableNode.Attributes["table:name"].Value == this.sheetName)
                {

                    XmlNodeList rowNodes = tableNode.SelectNodes("table:table-row", nmsManager);
                    if (rowNodes.Count > 0)
                    {

                        bool correctHeaderFormat = false;

                        // Check header row and its data
                        XmlNodeList cellMainNodes = rowNodes[0].SelectNodes("table:table-cell", nmsManager);
                        if (cellMainNodes.Count == columnTypes.GetLength(0))
                        {

                            for (int i = 0; i < cellMainNodes.Count; i++)
                                if (columnTypes[i, 0] != this.ReadCellValue(cellMainNodes[i]))
                                {
                                    break;
                                }
                                else if (cellMainNodes.Count == i + 1)
                                {
                                    correctHeaderFormat = true;
                                }

                        }


                        if (correctHeaderFormat)
                            correctFileFormat = true;
                        else
                            continue;

                    }


                    // Sheet has correct format, read movies items
                    for (int i = 1; i < rowNodes.Count; i++)
                    {

                        XmlNodeList cellNodes = rowNodes[i].SelectNodes("table:table-cell", nmsManager);
                        if (cellNodes.Count == columnTypes.GetLength(0))
                        {

                            media.Add(new Media()
                            {
                                OriginalName = this.ReadCellValue(cellNodes[0]),
                                TranslatedName = this.ReadCellValue(cellNodes[1]),
                                Country = this.ReadCellValue(cellNodes[2]),
                                ReleaseDate = Convert.ToDateTime(this.ReadCellValue(cellNodes[3])),
                                Length = Convert.ToUInt32(this.ReadCellValue(cellNodes[4])),
                                Director = this.ReadCellValue(cellNodes[5]),
                                Rating = Convert.ToDouble(this.ReadCellValue(cellNodes[6]), CultureInfo.CreateSpecificCulture("en-US")),
                                Genres = this.ParseItems(this.ReadCellValue(cellNodes[7])),
                                Cast = this.ParseItems(this.ReadCellValue(cellNodes[8])),
                                Content = this.ReadCellValue(cellNodes[9])
                            });

                        }
                        else
                        {
                            correctFileFormat = false;
                            break;
                        }
                    }


                }
            }


            if (!correctFileFormat)
                throw new Exception("Chosen file has incorrect format.");

            // return found movie medias
            return media;
        }


		/// <summary>
		/// retrieves the <c>ZipFile</c> from the stream
		/// </summary>
		/// <param name="stream">a <c>Stream</c> class from which the <c>ZipFile</c> should be obtained</param>
		/// <returns>the obtained <c>ZipFile</c></returns>
        private ZipFile GetZipFile(Stream stream)
        {
            return ZipFile.Read(stream);
        }

		/// <summary>
		/// retrieves the <c>ZipFile</c> from the file on the give path
		/// </summary>
		/// <param name="inputFilePath">the filePath from which the archive should be obtained</param>
		/// <returns>the obtained <c>ZipFile</c></returns>
        private ZipFile GetZipFile(string inputFilePath)
        {
            return ZipFile.Read(inputFilePath);
        }

		/// <summary>
		/// Retrieves the XML Document that is stored in a given <c>ZipFile</c>
		/// </summary>
		/// <param name="zipFile">the <c>ZipFile</c> from which the XML document should be retrieved</param>
		/// <returns></returns>
        private XmlDocument GetContentXmlFile(ZipFile zipFile)
        {

            ZipEntry contentZipEntry = zipFile["content.xml"];
            Stream contentStream = new MemoryStream();
            contentZipEntry.Extract(contentStream);
            contentStream.Seek(0, SeekOrigin.Begin);

            XmlDocument contentXml = new XmlDocument();
            contentXml.Load(contentStream);

            return contentXml;
        }

		/// <summary>
		/// reads the Table nodes within the document namespace
		/// </summary>
		/// <param name="contentXmlDocument">The XML document containing nodes that are to be retrieved</param>
		/// <param name="nmsManager">an <c>XmlNamespaceManager</c> class containing the namespace in which the NodeList
		/// should be searched for</param>
		/// <returns>the list of all XML nodes contained in a given namespace of the XML document</returns>
        private XmlNodeList GetTableNodes(XmlDocument contentXmlDocument, XmlNamespaceManager nmsManager)
        {
            return contentXmlDocument.SelectNodes("/office:document-content/office:body/office:spreadsheet/table:table", nmsManager);
        }

		/// <summary>
		/// Reads a certain cell value from the XML node and returns it as a string
		/// </summary>
		/// <param name="cell">the <c>XmlNode</c> correponding to the cell that is to be read</param>
		/// <returns></returns>
        private string ReadCellValue(XmlNode cell)
        {

            XmlAttribute cellVal = cell.Attributes["office:value"];

            if (cellVal == null)
            {
                cellVal = cell.Attributes["office:date-value"];
                if (cellVal == null)
                {
                    return String.IsNullOrEmpty(cell.InnerText) ? "" : cell.InnerText;
                }
                else
                    return cellVal.Value;
            }
            else
                return cellVal.Value;
        }


		/// <summary>
		/// support method to parse multiple value cells such as <value>cast</value> or <value>genre</value>
		/// </summary>
		/// <param name="value">the string that is to be parsed</param>
		/// <returns>a collection of parsed strings</returns>
        private IList<string> ParseItems(string value)
        {

            string[] list = value.Split('|');
            for (int i = 0; i < list.Length; i++)
                list[i] = list[i].Trim();

            return new List<string>(list);
        }

		/// <summary>
		/// removes all children of a root element in the XML document and returns the root
		/// </summary>
		/// <param name="contentXml">the XML document that should be processed</param>
		/// <param name="nmsManager">the namespace manager to work with</param>
		/// <returns></returns>
        private XmlNode GetSheetsRootNodeAndRemoveChildrens(XmlDocument contentXml, XmlNamespaceManager nmsManager)
        {
            XmlNodeList tableNodes = this.GetTableNodes(contentXml, nmsManager);

            XmlNode sheetsRootNode = tableNodes.Item(0).ParentNode;
            // remove sheets from template file
            foreach (XmlNode tableNode in tableNodes)
                sheetsRootNode.RemoveChild(tableNode);

            return sheetsRootNode;
        }


        #endregion
    }
}